from PYQT_db.utils.ImageModel import ImageModel
import pickle
import re
import time
import torch

import cv2
import numpy as np
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from facenet_pytorch import InceptionResnetV1
from ultralytics import YOLO
from FaceNet_ui import Ui_MainWindow
from PYQT_db.utils.CustomDelegate import CustomDelegate
from CameraDatabaseThreads import MySqlThread,cameraThread
from PYQT_db.config import YOLOV8_MODEL_PATH,LOGO_PATH, BIG_IMAGE_PATH,SMALL_IMAGE_PATH,YOLOV8_CLS_MODEL_PATH

class MyWindow(QMainWindow, Ui_MainWindow):
    def __init__(self,parent=None):
        super( MyWindow,self).__init__(parent)

        self.setupUi(self)
        # 界面部分功能初始化
        self.ui_init()
        # 数据库初始化
        self.db_init()
        # 初始化人脸特征变量
        self.save_face_feature = None
        # 开关线程的标志位
        self.count = 0
        # 是否保存照片标志位
        self.save_pic_flag = False

        # 加载 FaceNet 模型，用于提取人脸特征
        self.facenet_model = InceptionResnetV1(pretrained='casia-webface').eval().to('cpu')
        # YOLOv8 模型位置
        self.yolov8_model_path = YOLOV8_MODEL_PATH
        self.yolov8_model = YOLO(self.yolov8_model_path ).to('cpu')  # 主线程加载模型
        # 加载、预热模型
        self.preheat_model()

        #加载表情分类模型
        self.yolov8_cls_model = YOLO(YOLOV8_CLS_MODEL_PATH)
        #预热模型
        self.preheat_cls_model()

        self.camera_thread = cameraThread(
            yolov8_model=self.yolov8_model,
            facenet_model=self.facenet_model
        )
        # 绑定线程信号（确保UI更新正常）
        self.camera_thread.send_result_img.connect(self.show_camera_img)
        self.camera_thread.send_original_frame.connect(self.show_crop_face)

        # 初始化槽函数
        self.slot_init()
        # 数据库数据界面加载
        self.table_view_init()

    def ui_init(self):
        """
        初始化UI界面
        """
        # # 设置界面大小，固定为 1081, 762
        self.setFixedSize(1081, 762)
        # 默认显示人脸信息录入界面
        self.stackedWidget.setCurrentIndex(0)
        # 设置图标
        icon = QIcon()
        icon.addPixmap(QPixmap(LOGO_PATH), QIcon.Normal, QIcon.Off)
        self.setWindowIcon(icon)
        # 设置LCD显示位数
        self.time_show.setDigitCount(8)
        # 将计数值转换为时间格式
        self.start_time = QDateTime.currentDateTime().toString("hh:mm:ss")
        # 显示时间
        self.time_show.display(self.start_time)
        # 将识别记录edit设置为只读状态
        self.textEdit.setReadOnly(True)

    def db_init(self):
        """
        开启数据库线程
        """
        # 创建数据库线程实例
        self.my_sql = MySqlThread()
        # 建立信号与槽的连接
        self.my_sql.query_result_signal.connect(self.inquire_update_model)
        self.my_sql.signal_sql_data.connect(self.get_db_data)
        self.my_sql.finished_signal.connect(self.table_view_init)
        # 启动数据库线程
        self.my_sql.start()

    def slot_init(self):
        """
        初始化槽函数p
        :return:
        """
        # 人脸信息录入界面按键
        self.entry_btn_1.clicked.connect(self.entry_btn_slot)
        # 人脸识别按键切换界面
        self.entry_btn_2.clicked.connect(self.recognition_btn_slot)
        # 数据管理按键切换界面
        self.entry_btn_3.clicked.connect(self.management_btn_slot)
        # 退出按键
        self.entry_btn_4.clicked.connect(self.exit_btn_slot)

        # 打开文件按键
        self.pushButton.clicked.connect(self.open_folder_btn_slot)
        # 打开摄像头按键
        self.pushButton_2.clicked.connect(self.open_cap_btn_slot)
        # 拍照按键
        self.pushButton_3.clicked.connect(self.save_pic_btn_slot)
        # 关闭摄像头按键
        self.pushButton_4.clicked.connect(self.close_cap_btn_slot)
        # 保存输入的个人信息
        self.pushButton_5.clicked.connect(self.save_person_info_btn_slot)

        # 识别页面
        self.pushButton_9.clicked.connect(self.recognition_open_folder_btn_slot)
        self.pushButton_10.clicked.connect(self.recognition_open_cap_btn_slot)
        #表情抓捕
        self.pushButton_11.clicked.connect(self.recognition_capture_emotion_slot)
        self.pushButton_12.clicked.connect(self.recognition_close_cap_btn_slot)

        # 第三页
        # 查询
        self.pushButton_6.clicked.connect(self.inquire_sql_btn_slot)
        # 删除
        self.pushButton_7.clicked.connect(self.delete_row_btn_slot)
        # 显示全部数据
        self.pushButton_8.clicked.connect(self.table_view_init)

        # 初始化线程
        self.camera_thread = cameraThread(self.yolov8_model,self.facenet_model)


        # 定时器部分
        self.update_LCD_timer = QTimer(self)
        # 设置超时时间  1S  参数单位是毫秒
        self.update_LCD_timer.setInterval(1000)
        # timeout 定时器产生的信号
        self.update_LCD_timer.timeout.connect(self.update_LCD_time_slot)
        # 定时器需要启动，否则不工作
        self.update_LCD_timer.start()

    def preheat_model(self):
        """
        创建一个简单的三维图片数组，对模型进行预热，避免程序打开时间过长
        :return:
        """
        # 假设图片大小为 640x480，每个像素有三个颜色通道（RGB）
        simple_image_array = np.random.randint(0, 256, (240, 320, 3), dtype=np.uint8)

        # 加载之后，预热模型
        time1 = time.time()
        # 把模型放到设备上

        time2 = time.time()
        self.yolov8_model.predict(simple_image_array)
        time3 = time.time()
        print(f'模型加载时间{time2 - time1} 模型预测时间 {time3 - time2} 模型总预热时间：{time3 - time1}')

    #预热表情分类模型
    def preheat_cls_model(self):
        """预热表情分类模型"""
        simple_cls_img = np.random.randint(0, 256, (224, 224, 3), dtype=np.uint8)
        self.yolov8_cls_model.predict(simple_cls_img)
        print("表情分类模型预热完成")

    def entry_btn_slot(self):
        """
        人脸信息录入按键槽函数，切换界面
        """
        self.stackedWidget.setCurrentIndex(0)

    def recognition_btn_slot(self):
        """
        人脸识别按键槽函数，切换界面
        """
        self.stackedWidget.setCurrentIndex(1)

    def management_btn_slot(self):
        """
        数据管理按键槽函数，切换界面
        """
        self.stackedWidget.setCurrentIndex(2)


    def exit_btn_slot(self):

        reply = QMessageBox.question(
            self, "确认退出", "确定要退出系统吗？",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No
        )
        if reply == QMessageBox.Yes:
            QApplication.instance().quit()

    def open_folder_btn_slot(self):
        """
        人脸信息录入界面，打开文件对话框，选择图片文件，进行人脸检测
        """
        self.save_pic_path = QFileDialog.getOpenFileName(self, "打开", "./", "图片文件(*.jpg *.png *.gif *.ico)")[0]

        if self.save_pic_path:
            # 如果成功获取到路径, 调用V8检测, 坐标（左上角）和新人脸特征， 带框的图
            # q_image 添加了红框的原图    face_q_image 是裁剪后的人脸 pic_face_box坐标列表， new_face_feature人脸特征图列表
            self.q_image, self.face_q_image, self.pic_face_box, self.new_face_feature = self.recognize_show_images(
                self.save_pic_path)
            # 显示文件路径
            self.lineEdit.setText(self.save_pic_path)
            # 调用 大label 显示函数
            self.show_camera_img(self.q_image)
            # 通过坐标列表的长度，判断从图片中检测到了几张人脸信息，如果没有检测到，以及检测到大于一张人脸信息，那么报个警告！
            if len(self.pic_face_box) == 0:
                QMessageBox.warning(self, "人脸信息录入警告", f"图片中未检测到人脸信息，请重新上传图片！")
                # 恢复样式表的后背景
                self.label_2.setStyleSheet("""
                                            QLabel {
                                            border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                            border-image: url(./static/images/face_background1.jpg);
                                            }""")
                self.label_3.setStyleSheet("""
                                                    QLabel {
                                                    border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                                    border-image: url(:/images/face_background2.jpg);
                                                    }""")
                # 使用图像覆盖 label_2
                self.label_2.setPixmap(QPixmap(BIG_IMAGE_PATH).scaled(self.label_2.size()))
                self.label_3.setPixmap(
                    QPixmap(SMALL_IMAGE_PATH).scaled(self.label_3.size()))
            elif len(self.pic_face_box) == 1:
                # 从列表中获取人脸特征（检测后的）
                self.save_face_feature = self.new_face_feature[0]
                # 调用 小label 显示函数
                self.show_crop_face(self.face_q_image)
            else:

                QMessageBox.warning(self, "人脸信息录入警告",
                                    f"图片中检测到了 {len(self.pic_face_box)} 张人脸，请重新上传图片！")
                # 恢复样式表的后背景
                self.label_2.setStyleSheet("""
                                            QLabel {
                                            border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                            border-image: url(:/images/face_background1.jpg);
                                            }""")
                self.label_3.setStyleSheet("""
                                                    QLabel {
                                                    border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                                    border-image: url(:/images/face_background2.jpg);
                                                    }""")
                # 使用图像覆盖 label_2
                self.label_2.setPixmap(QPixmap(BIG_IMAGE_PATH).scaled(self.label_2.size()))
                self.label_3.setPixmap(
                    QPixmap(SMALL_IMAGE_PATH).scaled(self.label_3.size()))
        else:
            print('没有选择图片文件。')

    def open_cap_btn_slot(self):
        """
        开启摄像头线程和 YOLO 预测线程
        第一次是系统实例化了线程，当我们使用停止按键关闭摄像头后，如果再次打开摄像头，我们需要再次实例化新的线程，所以这里要跟第一次区分开
        :return:
        """
        try:
            # 检查线程是否已经在运行
            if self.camera_thread.isRunning():
                # 如果线程正在运行，弹出消息框
                QMessageBox.warning(self, "警告", "摄像头线程已经在运行中，请先关闭识别界面摄像头！")
                return

            if self.count == 0:
                # 连接槽函数
                self.camera_thread.send_result_img.connect(self.show_camera_img)
                self.camera_thread.send_original_frame.connect(self.save_qimage)
                self.camera_thread.start()
                self.count += 1
            elif self.count > 0:
                # 再次实例化线程
                self.camera_thread = cameraThread(self.yolov8_model,self.facenet_model)
                self.camera_thread.start()
                # 由于前面已经断开链接，所以需要重新链接槽函数
                self.camera_thread.send_result_img.connect(self.show_camera_img)
                self.camera_thread.send_original_frame.connect(self.save_qimage)
        except AttributeError:
            # 如果 camera_thread 还没有被实例化，捕获异常并处理
            QMessageBox.critical(self, "错误", "摄像头线程尚未初始化！")
            self.camera_thread = cameraThread(self.yolov8_model,self.facenet_model)
            self.camera_thread.send_result_img.connect(self.show_camera_img)
            self.camera_thread.send_original_frame.connect(self.save_qimage)
            self.camera_thread.start()
            self.count += 1

    def save_pic_btn_slot(self):
        # 保存拍照图的标志位
        self.save_pic_flag = True

    def save_qimage(self, frame):
        """
        保存视频帧的图像
        :return:
        """

        if self.save_pic_flag:
            self.save_pic_path = '../saved_picture/save_pic.jpg'
            # 参数：1、路径， 2、格式， 图片的质量因数，范围必须在 0 到 100 之内或 -1，指定0以获取小型压缩文件，指定100表示大型未压缩文件，使用-1(默认值)使用默认设置
            frame.save(self.save_pic_path, 'JPG', -1)
            # 保存后标志位重新置为 False
            self.save_pic_flag = False
            # q_image 添加了红框的原图    face_q_image 是裁剪后的人脸 pic_face_box坐标列表， new_face_feature人脸特征图列表
            self.q_image, self.face_q_image, self.pic_face_box, self.new_face_feature = self.recognize_show_images(
                self.save_pic_path)
            # 通过坐标列表的长度，判断从图片中检测到了几张人脸信息，如果没有检测到，以及检测到大于一张人脸信息，那么报个警告！
            if len(self.pic_face_box) == 0:
                QMessageBox.warning(self, "人脸信息录入警告", f"拍照后的图片未检测到人脸信息，请重新拍照！")
            elif len(self.pic_face_box) == 1:
                # 从列表中获取人脸特征（检测后的）
                self.save_face_feature = self.new_face_feature[0]
                # 调用 小label 显示函数
                self.show_crop_face(self.face_q_image)
            else:
                QMessageBox.warning(self, "人脸信息录入警告",
                                    f"拍照后的图片检测到了 {len(self.pic_face_box)} 张人脸，请重新拍照！")

    def close_cap_btn_slot(self):
        """
        关闭摄像头按键槽函数
        :return:
        """
        try:
            # 检查线程是否已经在运行
            if not self.camera_thread.isRunning():
                # 如果 camera_thread 还没有被实例化，捕获异常并处理
                QMessageBox.critical(self, "错误", "摄像头还未开启！")
                return
            # 先断开信号链接，不然还会有摄像头的资源传过来
            # 然后再停止线程
            # 最后使用背景图覆盖
            self.camera_thread.send_result_img.disconnect(self.show_camera_img)
            self.camera_thread.send_original_frame.disconnect(self.save_qimage)
            self.camera_thread.stop()

            # 恢复样式表的后背景
            self.label_2.setStyleSheet("""
                                        QLabel {
                                        border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                        border-image: url(:/images/face_background1.jpg);
                                        }""")
            self.label_3.setStyleSheet("""
                                                QLabel {
                                                border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                                border-image: url(:/images/face_background2.jpg);
                                                }""")
            # 使用图像覆盖 label_2
            self.label_2.setPixmap(QPixmap(BIG_IMAGE_PATH).scaled(self.label_2.size()))
            self.label_3.setPixmap(
                QPixmap(SMALL_IMAGE_PATH).scaled(self.label_3.size()))
        except Exception as e:
            print(f"发生属性错误: {e}")

    def save_person_info_btn_slot(self):
        # 获取用户输入的数据
        # 从界面获取 名字
        name = self.lineEdit_2.text()
        # 获取年龄
        age = self.lineEdit_3.text()
        # 获取性别
        sex = "男" if self.radioButton.isChecked() else "女" if self.radioButton_2.isChecked() else None
        # 获取学号
        studentID = self.lineEdit_4.text()
        # 获取时间
        current_time = QDateTime.currentDateTime().toString("yyyy-MM-dd HH:mm:ss")

        # 输入验证
        if not name or not studentID:
            QMessageBox.critical(self, "错误", "姓名和学号不能为空！")
            return

        try:
            age = int(age)  # 确保年龄是整数
        except ValueError:
            QMessageBox.critical(self, "错误", "年龄必须是数字！")
            return

        # 将保存下来的图片读取为二进制格式，准备存入数据库
        try:
            with open(self.save_pic_path, 'rb') as f:
                blob_data = f.read()
        except Exception as e:
            QMessageBox.critical(self, "错误", f"读取图片时发生错误: {str(e)}")
            return

        # 将人脸特征数据序列化为字节流，以便于存储或传输
        self.save_face_feature_bytes = pickle.dumps(self.save_face_feature)
        # 用于调试
        # print(f'存数据库之前的维度：{self.save_face_feature.shape}')
        # print('存入前的长度：', len(self.save_face_feature_bytes))
        # PyQt 的环境中使用的话，需要将 JSON 字符串转化为 QByteArray，方便后续在信号中传递

        # 保存输入的信息到数据库
        self.my_sql.q_deal_sql_cmd.put({
            "cmd": self.my_sql.INSERT_NEW,
            "content": {
                "name": name,
                "sex": sex,
                "age": age,
                "studentID": studentID,
                "time": current_time,
                "img": QByteArray(blob_data),
                "facefeature": QByteArray(self.save_face_feature_bytes)
            }
        })

        # 清空写入的数据
        self.lineEdit_2.clear()
        self.lineEdit_3.clear()
        self.lineEdit_4.clear()
        # 清空上传路径
        self.lineEdit.clear()

    def recognition_open_folder_btn_slot(self):
        """
        打开文件对话框，选择图片文件，进行人脸识别
        :return:
        """
        self.recognition_path = QFileDialog.getOpenFileName(self, "打开", "./", "图片文件(*.jpg *.png *.gif *.ico)")[0]
        # 用于存放待对比的人脸信息
        self.face_info = []
        if self.recognition_path:
            # 如果成功获取到路径, 调用V8检测, 坐标（左上角）和新人脸特征， 带框的图
            #  q_image 添加了红框的原图    face_q_image 是裁剪后的人脸 pic_face_box坐标列表， new_face_feature人脸特征图列表
            self.q_image, self.face_q_image, self.pic_face_box, self.new_face_feature = self.recognize_show_images(
                self.recognition_path)
            for i in range(len(self.pic_face_box)):
                face_box = self.pic_face_box[i]
                new_face_feature = self.new_face_feature[i]
                self.face_info.append([face_box, new_face_feature])
            # 显示文件路径
            self.lineEdit_6.setText(self.recognition_path)
            # 发送命令查询数据库内容
            self.my_sql.q_deal_sql_cmd.put({"cmd": self.my_sql.SELECT_FACE_FEATURE})

        else:
            print('没有选择文件。')

    def recognition_capture_emotion_slot(self):
        """表情捕捉按钮槽函数"""
        # 获取当前摄像头帧（从camera_thread中获取最新帧）
        if hasattr(self.camera_thread, 'frame') and self.camera_thread.frame is not None:
            frame = self.camera_thread.frame.copy()
            # 获取已检测到的人脸框
            face_boxes = self.camera_thread.face_boxes

            if not face_boxes:
                QMessageBox.warning(self, "警告", "未检测到人脸，请先对准人脸")
                return

            # 对每个检测到的人脸进行表情分类
            emotion_results = []
            for (x1, y1) in face_boxes:
                # 从框坐标获取人脸区域（假设框坐标是左上角，需结合框大小）
                # 这里简化处理，实际应使用完整框坐标(x1,y1,x2,y2)
                x2 = x1 + 200  # 临时值，需根据实际框大小调整
                y2 = y1 + 200
                face_img = frame[int(y1):int(y2), int(x1):int(x2)]

                if face_img.size == 0:
                    continue

                # 预处理人脸图像（适应分类模型输入）
                face_img = cv2.resize(face_img, (224, 224))
                face_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)

                # 表情分类预测
                results = self.yolov8_cls_model.predict(face_img, conf=0.7)
                if results:
                    # 获取预测结果（假设模型输出类别为表情标签）
                    top1 = results[0].probs.top1
                    top1_conf = results[0].probs.top1conf.item()
                    emotion = results[0].names[top1]
                    emotion_results.append(f"表情：{emotion} (置信度：{top1_conf:.2f})")

            # 在文本编辑框显示结果
            if emotion_results:
                self.textEdit.append("=== 表情捕捉结果 ===")
                for res in emotion_results:
                    self.textEdit.append(res)
            else:
                self.textEdit.append("未识别到有效表情")
        else:
            QMessageBox.warning(self, "警告", "请先打开摄像头")

    def recognition_open_cap_btn_slot(self):
        """
        开启摄像头线程和 YOLO 预测线程
        第一次是系统实例化了线程，当我们使用停止按键关闭摄像头后，如果再次打开摄像头，我们需要再次实例化新的线程，所以这里要跟第一次区分开
        :return:
        """
        try:
            # 检查线程是否已经在运行
            if self.camera_thread.isRunning():
                # 如果线程正在运行，弹出消息框
                QMessageBox.warning(self, "警告", "摄像头线程已经在运行中，请先关闭人脸录入界面摄像头！")
                return

            if self.count == 0:
                # 连接识别界面的信号
                self.camera_thread.send_face_feature_box.connect(self.get_face_feature_box)
                self.camera_thread.start()
                self.count += 1
            elif self.count > 0:
                # 再次实例化线程，由于是再次实例化的线程，所以不需要断开录入界面的信号
                self.camera_thread = cameraThread(self.yolov8_model,self.facenet_model)
                self.camera_thread.send_face_feature_box.connect(self.get_face_feature_box)
                self.camera_thread.start()

        except AttributeError:
            # 如果 camera_thread 还没有被实例化，捕获异常并处理
            QMessageBox.critical(self, "错误", "摄像头线程尚未初始化！")
            self.camera_thread = cameraThread(self.yolov8_model,self.facenet_model)
            self.camera_thread.send_face_feature_box.connect(self.get_face_feature_box)
            self.camera_thread.start()
            self.count += 1

    def recognition_close_cap_btn_slot(self):
        """
        关闭摄像头按键槽函数
        :return:
        """
        try:
            # 检查线程是否已经在运行
            if not self.camera_thread.isRunning():
                # 如果 camera_thread 还没有被实例化，捕获异常并处理
                QMessageBox.critical(self, "错误", "摄像头还未开启！")
                return
            # 先断开信号链接，不然还会有摄像头的资源传过来
            # 然后再停止线程
            # 最后使用背景图覆盖
            self.camera_thread.send_face_feature_box.disconnect(self.get_face_feature_box)
            self.camera_thread.stop()

            # 恢复样式表的后背景
            self.label_11.setStyleSheet("""
                                        QLabel {
                                        border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                        border-image: url(:/images/face_background1.jpg);
                                        }""")
            # 使用图像覆盖 label_2
            self.label_11.setPixmap(
                QPixmap('./static/images/face_background_1.jpg').scaled(self.label_11.size()))
            # 清空识别结果
            self.textEdit.clear()
        except Exception as e:
            print(f"发生属性错误: {e}")

    def inquire_sql_btn_slot(self):
        """
        查询按钮槽函数
        :return:
        """
        # 给数据库线程发送命令查询， 去除首尾空格
        text = self.lineEdit_5.text().strip()
        # 定义正则表达式
        chinese_pattern = r'[\u4e00-\u9fff]'  # 汉字
        english_pattern = r'^[a-zA-Z]+$'  # 字母
        # 验证输入是否为空
        if not text:
            QMessageBox.warning(self, "警告", "查询关键字不能为空", QMessageBox.Ok)
            return

        # 验证输入是否包含汉字或只包含英文字母  re.IGNORECASE  忽略大小写
        if not (re.search(chinese_pattern, text) or re.fullmatch(english_pattern, text), re.IGNORECASE):
            QMessageBox.warning(self, "警告", "请输入有效的姓名（仅支持汉字或英文字母）")
            return

        # 将查询请求发送到数据库线程
        self.my_sql.q_deal_sql_cmd.put({"cmd": self.my_sql.SELECT_BY_KEYWORD, "content": {"name": text}})

    def delete_row_btn_slot(self):
        """
        删除选中行
        :return:
        """
        # 获取当前选中的模型索引
        index = self.tableView.currentIndex()
        if index.isValid():
            try:
                # 开启数据库事务
                self.my_sql.db.transaction()
                # 从模型中删除行
                self.model.removeRow(index.row())
                # 刷新模型
                self.model.select()
                # 提交更改到数据库
                if self.model.submitAll():
                    # 事务提交成功，使更改持久化到数据库
                    self.my_sql.db.commit()
                else:
                    # 获取错误信息
                    error = self.model.lastError()
                    # 出现错误，回滚事务
                    self.my_sql.db.rollback()
                # 发送
                self.my_sql.q_deal_sql_cmd.put({"cmd": self.my_sql.UPDATE_SORT})
                # 弹出消息提示框
                QMessageBox.information(self, "提示", "删除成功")
            except Exception as e:
                # 出现错误进行事务回滚，恢复到之前的数据库状态
                self.my_sql.db.rollback()

    def inquire_update_model(self, result):
        """
        更新查询后的模型
        :param result:
        :return:
        """
        # 如果没有查询到数据，提示用户
        if not result:
            QMessageBox.information(self, "提示", "未找到匹配的数据")
            return

        # 使用查询到的姓名作为过滤条件
        self.model.setFilter(f"姓名 LIKE '%{result[0][1]}%'")
        self.model.select()

    def update_LCD_time_slot(self):
        """
        更新LCD显示时间
        :return:
        """
        # 将计数值转换为时间格式
        self.start_time = QDateTime.currentDateTime().toString("hh:mm:ss")
        # 显示时间
        self.time_show.display(self.start_time)

    def table_view_init(self):

        """
        初始化 显示表格数据 的控件
        :return:
        """
        # 清空搜索框内容
        self.lineEdit_5.clear()
        # 设置一个文本输入控件的占位符文本。
        # 占位符文本是在用户没有输入任何内容时显示在输入框中的灰显文本，通常用于提示用户应该输入什么内容。
        self.lineEdit_5.setPlaceholderText("仅支持姓名查询")

        # 隐藏垂直表头
        self.tableView.verticalHeader().setVisible(False)
        # 获取水平表头的对象
        self.horizontal_header = self.tableView.horizontalHeader()
        # 设置字体和大小
        self.header_font = QFont("./static/fontFile/HarmonyOS_Sans_SC_Medium.ttf", 16, QFont.Bold)
        # 将创建的字体应用到表头
        self.horizontal_header.setFont(self.header_font)

        # 创建自定义模型
        self.model = ImageModel(self)
        self.model.setTable('facedatabase')
        self.model.select()

        # 把模型设置给表格视图
        self.tableView.setModel(self.model)

        # 获取第8列（索引为7），表头所在行（索引为0）的模型索引
        index = self.model.index(0, 7)
        # 隐藏第8列
        self.tableView.setColumnHidden(index.column(), True)

        # 设置自定义委托
        content_delegate = CustomDelegate(self.tableView)
        self.tableView.setItemDelegate(content_delegate)

        # 遍历所有行并设置高度
        for row in range(self.model.rowCount()):
            self.tableView.setRowHeight(row, 120)

        # 确保图片列的宽度为160
        self.tableView.setColumnWidth(6, 160)

        # 禁止用户调整宽度
        for col in range(self.model.columnCount()):
            self.horizontal_header.setSectionResizeMode(col, QHeaderView.Fixed)

        # 禁止用户调整高度
        self.tableView.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)

        # 设置滚动条
        self.tableView.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.tableView.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)



    def get_face_feature_box(self, face_boxes, face_features, q_image):
        """
        从摄像头线程获取以下信息
        :param face_box: 人脸坐标
        :param q_image: 带红框人脸图
        :param new_face_feature: 计算好的人脸特征的列表
        :return:
        """
        self.face_info = []
        self.q_image = q_image
        for i in range(len(face_boxes)):
            face_box = face_boxes[i]
            new_face_feature = face_features[i]
            self.face_info.append([face_box, new_face_feature])

        # 发送命令查询数据库人脸特征
        self.my_sql.q_deal_sql_cmd.put({"cmd": self.my_sql.SELECT_FACE_FEATURE})

    def get_db_data(self, sql_data_dict, threshold=0.75):
        """
        获取数据库人脸特征
        :param sql_data_dict:
        :return:
        """
        matched_names = []  # 用于存储匹配到的名字
        matched_coords = []  # 用于存储匹配到的名字对应的坐标
        matched_infos = []  # 用于存储匹配到的其他信息（性别、年龄、学号）
        # 未识别人脸计数器
        unknown_count = 0

        if self.my_sql.SELECT_FACE_FEATURE in sql_data_dict:
            for face_data in self.face_info:
                face_box, new_face_feature = face_data
                # 初始化最大相似度为-1.0，因为我们正在寻找最大值
                max_similarity = -1  # 初始设置为-1，因为余弦相似度的范围是[-1, 1]
                # 创建变量
                matched_name = "unknown"
                matched_info = ["", "", "", ""]

                # 待对比的图只需要计算一次，如果放到for中，那么每次循环都会计算，放到外面优化性能
                new_face_feature_norm = np.linalg.norm(new_face_feature)

                for data in sql_data_dict[self.my_sql.SELECT_FACE_FEATURE]:
                    try:
                        db_face_feature = pickle.loads(data[4])
                        # 计算数据库里人脸特征的范数
                        db_face_feature_norm = np.linalg.norm(db_face_feature)
                        # 打印调试
                        # print(f'取出来的维度{self.db_face_feature.shape}')
                        # 计算输入特征向量与数据库中特征向量的余弦相似度
                        # 余弦相似度计算公式是两个向量的点积除以它们的范数的乘积
                        similarity = np.dot(new_face_feature, db_face_feature.T) / (
                                new_face_feature_norm * db_face_feature_norm)
                        # 打印相似度，用于调试
                        # print(f'similarity: {similarity}')
                        # 如果当前相似度大于已记录的最大相似度，则更新最大相似度和匹配的人名
                        if similarity > max_similarity:
                            # 更新最大相似度
                            max_similarity = similarity
                            matched_name = data[0]
                            # print("max_similarity", max_similarity)
                            # 性别、年龄、学号
                            matched_info = [data[1], str(data[2]), str(data[3]), max_similarity]

                    except Exception as e:
                        print(f"处理数据错误: {e}")
                        continue
                # 如果从数据库中对比的相似度结果大于给定阈值，则认为是同一个人
                if max_similarity >= threshold:
                    matched_names.append(matched_name)
                    matched_coords.append(face_box)  # 添加匹配到的名字的对应的坐标
                    matched_infos.append(matched_info)

                else:
                    matched_names.append("unknown")
                    matched_coords.append(face_box)
                    matched_infos.append(["", "", "", ""])

            # 每次显示之前清空记录
            self.textEdit.clear()
            # 更新UI显示匹配到的信息
            # 计算未知人脸数量
            if matched_names:
                unknown_count = sum(1 for name in matched_names if name == "unknown")

            # 汇总信息，显示总人脸数和未知人脸数
            total_faces = len(matched_names)
            summary_text = f"本次总共检测到 {total_faces} 张人脸，其中 {unknown_count} 张人脸未识别成功。\n"
            self.textEdit.append(summary_text)
            self.textEdit.ensureCursorVisible()

            if matched_names:
                for i in range(len(matched_names)):
                    if matched_names[i] == "unknown":
                        unknown_count += 1
                    else:
                        # 获取数组中的值（假设数组只有一个元素，根据实际情况调整索引等）
                        similarity_value = matched_infos[i][3][0] if isinstance(matched_infos[i][3], np.ndarray) else \
                            matched_infos[i][3]
                        similarity_str = "{:.2f}".format(similarity_value[0])
                        # 获取时间
                        get_current_time = QDateTime.currentDateTime().toString("yyyy-MM-dd HH:mm:ss")
                        info_text = f"姓名: {matched_names[i]}\n年龄: {matched_infos[i][1]}\n性别: {matched_infos[i][0]}\n学号: {matched_infos[i][2]}\n最大相似度: {similarity_str}\n识别时间：{get_current_time}\n"
                        self.textEdit.append(info_text)
                        # 追加完内容之后确保光标可见
                        self.textEdit.ensureCursorVisible()

            # 在图上绘制名字
            self.draw_result(self.q_image, matched_coords, matched_names)

    def draw_result(self, q_image, matched_coords, matched_names):
        # 加载自定义字体
        font_id = QFontDatabase.addApplicationFont(":/fontFile/HarmonyOS.ttf")
        font_family = QFontDatabase.applicationFontFamilies(font_id)[0]

        # 根据目标 QLabel 的大小缩放 QImage
        scaled_size = self.label_11.size()
        scaled_image = q_image.scaled(scaled_size, aspectRatioMode=Qt.KeepAspectRatio,
                                      transformMode=Qt.SmoothTransformation)
        # 计算缩放比例
        scale_ratio_width = scaled_image.width() / q_image.width()
        scale_ratio_height = scaled_image.height() / q_image.height()

        # 创建一个QImage用于绘制文本，大小与缩放后的图像相同
        text_image = QImage(scaled_image.size(), QImage.Format_ARGB32)
        text_image.fill(Qt.transparent)
        painter = QPainter(text_image)
        painter.setPen(QColor(255, 0, 0))  # 红色文本

        # 设置字体和大小
        font = QFont(font_family)
        font.setPointSize(20)  # 使用固定的点大小
        painter.setFont(font)

        # 遍历所有匹配的名字和坐标，绘制到图像上
        for coord, name in zip(matched_coords, matched_names):
            # 计算缩放后的 x 和 y 坐标
            scaled_x = coord[0] * scale_ratio_width
            scaled_y = coord[1] * scale_ratio_height - 10
            # 绘制文本
            painter.drawText(int(scaled_x), int(scaled_y), name)

        painter.end()
        # 将文本图像绘制到缩放后的图像上
        painter.begin(scaled_image)
        painter.drawImage(0, 0, text_image)
        painter.end()

        # 更改 label_2 样式表
        self.label_11.setStyleSheet("""
            QLabel {
                border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                border-image: none;  /* 移除背景图 */
            }""")

        # 显示图像，不进行额外缩放
        self.label_11.setPixmap(QPixmap.fromImage(scaled_image))

    def preprocess_face_img(self, face_img):
        """
        对人脸图像进行预处理
        :param face_img: 需要处理的图像
        :return:返回处理图像后的 PyTorch 张量
        """
        # 将BGR图像转换为RGB图像
        face_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)
        # 定义 FaceNet 模型所需的输入图像尺寸
        required_size = (160, 160)
        # 使用OpenCV调整图像尺寸到模型期望的尺寸
        face_img = cv2.resize(face_img, required_size)
        # 将图像数据转换为 PyTorch 张量
        # permute(2, 0, 1)将图像的维度从HWC (高度、宽度、通道) 转换为CHW (通道、高度、宽度)
        # float()将张量数据类型转换为浮点数
        # 将图像转换为Tensor格式，并归一化
        # unsqueeze(0)在张量前面添加一个批次维度，因为模型期望输入是4维的 [B, C, H, W]
        face_tensor = (torch.tensor(face_img).permute(2, 0, 1).float() / 255.0).unsqueeze(0)
        # 返回图像归一化后的 PyTorch 张量
        return face_tensor

    def extract_face_feature(self, face_tensor):
        """
        提取人脸特征
        :param face_tensor: 图像预处理后的 PyTorch 张量
        :return: 返回 face_embedding 人脸特征
        """
        # 使用 torch.no_grad() 上下文管理器，表示告诉 PyTorch 不需要计算梯度
        # 这通常用于推理阶段，可以减少内存消耗和提高速度
        with torch.no_grad():
            # 将处理后的图像张量传递给 FaceNet 模型以提取特征
            face_embedding = self.facenet_model(face_tensor)
            # 对提取的人脸特征进行L2归一化
            # 计算特征向量的L2范数
            l2_norm = torch.norm(face_embedding, p=2, dim=1, keepdim=True)
            # 将特征向量除以其L2范数进行归一化
            face_embedding_normalized = face_embedding.div(l2_norm)
        # 将得到的特征张量移动到CPU上，并转换为 NumPy 数组
        # 这一步是为了后续处理，如保存特征或进行其他非 PyTorch 操作
        return face_embedding_normalized.cpu().numpy()

    def recognize_show_images(self, file_path):
        """
        读取图像(不是摄像头)并检测人脸
        :param file_path: 路径
        :return:
        """
        # 用于存储多个人脸的边界框坐标列表
        self.pic_face_boxes = []
        # 用于存储多个人脸的特征列表
        self.pic_face_features = []
        # 初始化人脸图像变量，避免未定义
        face_img = None
        # 使用 cv2.imread() 函数读取图片
        self.image = cv2.imread(file_path)
        if self.image is None:
            QMessageBox.warning(self, "错误", "无法读取图像文件")
            return None, None, [], []

        # YOLOv8 进行人脸检测
        self.image_result = self.yolov8_model.predict(self.image, conf=0.7)
        # 获取检测结果中的边界框信息
        self.boxes = self.image_result[0].boxes
        # 遍历检测到的人脸框列表
        for self.box in self.boxes:
            # 解包人脸框的坐标信息
            x1, y1, x2, y2 = self.box.xyxy[0].tolist()
            # 提取类别标签（0代表人脸）
            cls = self.box.cls[0].tolist()
            if cls == 0:
                # 裁剪人脸区域（使用copy()避免原图修改影响）
                face_img = self.image[int(y1):int(y2), int(x1):int(x2)].copy()
                # 绘制人脸框
                cv2.rectangle(self.image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2)
                # 记录人脸框坐标
                self.pic_face_boxes.append([x1, y1])
                # 检查裁剪的人脸是否有效
                if face_img.shape[0] > 0 and face_img.shape[1] > 0:
                    # 预处理并提取特征
                    face_tensor = self.preprocess_face_img(face_img)
                    new_face_feature = self.extract_face_feature(face_tensor)
                    self.pic_face_features.append(new_face_feature)

        # 处理原始图像为QImage
        h, w, ch = self.image.shape
        bytes_per_line = ch * w
        self.q_image = QImage(self.image.data, w, h, bytes_per_line, QImage.Format_BGR888)

        # 处理人脸图像为QImage（仅当检测到人脸时）
        self.face_q_image = None
        if face_img is not None and face_img.shape[0] > 0 and face_img.shape[1] > 0:
            h, w, ch = face_img.shape
            face_bytes_per_line = ch * w
            self.face_q_image = QImage(face_img.data, w, h, face_bytes_per_line, QImage.Format_BGR888)

        return self.q_image, self.face_q_image, self.pic_face_boxes, self.pic_face_features
    def show_camera_img(self, rgb_img):
        """
        显示检测后的带红框的视频
        :param rgb_img:
        :return:
        """
        # 更改 label_2 样式表
        self.label_2.setStyleSheet("""
                                    QLabel {
                                    border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                    border-image: none;  /* 移除背景图 */
                                    }""")
        # 显示经过 YOLOV8 模型预测后的画面，大图
        self.label_2.setPixmap(
            QPixmap.fromImage(rgb_img).scaled(self.label_2.size(), aspectRatioMode=Qt.KeepAspectRatio))

    def show_crop_face(self, face_img):
        """
        用于显示检测后的裁剪出来的人脸图像
        :param face_img:
        :return:
        """
        # 清除背景图，截取后的图
        self.label_3.setStyleSheet("""
                                            QLabel {
                                            border: 2px solid #214a9c;  /* 设置边框宽度、颜色和样式 */
                                            border-image: none;  /* 移除背景图 */
                                            }""")
        # 显示剪裁后的图
        self.label_3.setPixmap(
            QPixmap.fromImage(face_img).scaled(self.label_3.size(), aspectRatioMode=Qt.KeepAspectRatio))

    # 在database_ui.py的MyWindow类中添加
    def closeEvent(self, event):
        if hasattr(self, 'camera_thread') and self.camera_thread.isRunning():
            self.camera_thread.stop()
        if hasattr(self, 'my_sql') and self.my_sql.isRunning():
            self.my_sql.running = False
            self.my_sql.wait()
        event.accept()